// Copyright (c) Microsoft Corporation. All rights reserved.

using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.TeamFoundation.VersionControl.Client;
using Microsoft.TeamFoundation.Client;
using System.Net;
using System.IO;
using System.Threading;
using System.Diagnostics;
using System.Globalization;

namespace Microsoft.TeamFoundation.Migration.Toolkit.VC
{
    public abstract class SourceToTfsMigrationEngine : VCMigrationEngine
    {
        public event EventHandler<MigrationEventArgs> MigrationStarting;
        public event EventHandler<MigrationEventArgs> MigrationComplete;
        public event EventHandler<MigrationEventArgs> RetryStarting;

        protected SourceToTfsMigrationEngine(VersionControlSession session, ChangeGroupingMananger manager)
            : base(session, manager)
        {

            string serverKey = getProperEndPoint(session).Server;

            TeamFoundationServer tfsServer = TeamFoundationServerFactory.GetServer(MigrationConfiguration.TfsServers[serverKey].Server);
            m_tfsClient = (VersionControlServer)tfsServer.GetService(typeof(VersionControlServer));

            MigrationStarting += new EventHandler<MigrationEventArgs>(BeforeMigration);
            MigrationComplete += new EventHandler<MigrationEventArgs>(AfterMigrate);
            RetryStarting += new EventHandler<MigrationEventArgs>(BeforeRetry);



            m_completedTfs2SourceMigrationHWM = new HighWaterMark<int>(session, "TFSMigrationHWM");
            m_completedTfs2SourceMigrationHWM.Reload();
        }

        private bool useTargetPoint
        {
            get
            {
                return Manager.Direction == MigrationDirection.SourceToTfs;
            }
        }

        private static TfsVersionControlTargetEndpoint getProperEndPoint(VersionControlSession session)
        {
            return session.Target;
        }

        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Security", "CA2109:ReviewVisibleEventHandlers")]
        protected virtual void SingleItemError(object sender, BatchedItemEventArgs e)
        {
            throw new VersionControlMigrationException(
                string.Format(
                    MigrationToolkitVCResources.Culture,
                    MigrationToolkitVCResources.SingleItemActionFailed,
                    e.Target.Action, e.Target.Target),e.Exception);
        }

        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Security", "CA2109:ReviewVisibleEventHandlers")]
        protected virtual void SingleItemWarning(object sender, BatchedItemEventArgs e)
        {
            TraceManager.TraceInformation(e.Message);
        }


        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Security", "CA2109:ReviewVisibleEventHandlers")]
        protected virtual void MergeError(object sender, BatchedItemEventArgs e)
        {
            throw new VersionControlMigrationException(
                string.Format(
                    MigrationToolkitVCResources.Culture,
                    MigrationToolkitVCResources.MultiItemActionFailed,
                    e.Target.Action, e.Target.Source, e.Target.Target), e.Exception);
        }

        public override void Run()
        {
            OnBeforeMigrate();

            base.Run();

            OnAfterMigrate();
        }

        private void OnBeforeMigrate()
        {
            if (MigrationStarting != null)
            {
                MigrationStarting(this, new MigrationEventArgs(MigrationToolkitResources.DefaultEventString));
            }
        }

        private void OnAfterMigrate()
        {
            if (MigrationComplete != null)
            {
                MigrationComplete(this, new MigrationEventArgs(MigrationToolkitResources.DefaultEventString));
            }
        }

        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Security", "CA2109:ReviewVisibleEventHandlers")]
        protected virtual void AfterMigrate(object sender, MigrationEventArgs e)
        {
            TfsUtil.CleanWorkspaceFiles(Workspace);
            Workspace.Delete();
        }

        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Security", "CA2109:ReviewVisibleEventHandlers")]
        protected virtual void BeforeRetry(object sender, MigrationEventArgs e)
        {
        }

        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Security", "CA2109:ReviewVisibleEventHandlers")]
        protected virtual void BeforeMigration(object sender, MigrationEventArgs e)
        {
            Workspace = CreateWorkspace();
        }


        protected virtual VersionControlServer TfsClient
        {
            get
            {
                return m_tfsClient;
            }
        }

        protected override void ProcessChangeGroup(ChangeGrouping group)
        {
            TfsUtil.CleanWorkspace(Workspace);

            group.Status = ChangeStatus.InProgress;
            group.Save();

            BatchingContext ctx = new BatchingContext(Workspace);

            ctx.BatchedItemError += SingleItemError;
            ctx.BatchedItemWarning += SingleItemWarning;
            ctx.MergeError += MergeError;

            foreach (MigrationAction action in group.Actions)
            {
                Session.ThrowIfAborted();

                if (action.State != ActionState.Pending)
                {
                    continue;
                }

                switch (action.Action)
                {
                    case ChangeAction.Add:
                        Add(action, ctx);
                        break;
                    case ChangeAction.Branch:
                        Branch(action, ctx);
                        break;
                    case ChangeAction.Delete:
                        Delete(action, ctx);
                        break;
                    case ChangeAction.Edit:
                        Edit(action, ctx);
                        break;
                    case ChangeAction.Encoding:
                        Encoding(action, ctx);
                        break;
                    case ChangeAction.Merge:
                        Merge(action, ctx);
                        break;
                    case ChangeAction.Rename:
                        Rename(action, ctx);
                        break;
                    case ChangeAction.Undelete:
                        Undelete(action, ctx);
                        break;
                    case ChangeAction.Label:
                        Label(action, ctx);
                        break;
                    default:
                        Unknown(action, ctx);
                        break;
                }
            }

            Flush(ctx);
            int changesetId;

            if (Checkin(group, out changesetId))
            {
                group.Status = ChangeStatus.Complete;
                group.Save();

                foreach (MigrationAction action in group.Actions)
                {
                    if (action.State == ActionState.Pending)
                    {
                        action.State = ActionState.Complete;
                    }
                }
                string sourceChangeId;
                int tfsChangesetId;
                if (Direction == MigrationDirection.SourceToTfs)
                {
                    sourceChangeId = group.Name;
                    tfsChangesetId = changesetId;
                }
                else
                {
                    sourceChangeId = changesetId.ToString(CultureInfo.InvariantCulture);
                    tfsChangesetId = int.Parse(group.Name, CultureInfo.InvariantCulture);
                }
                UpdateConversionHistory(
                    group.SessionId,
                    sourceChangeId,
                    tfsChangesetId,
                    DateTime.UtcNow,
                    group.Comment,
                    Direction);
            }
        }

        protected virtual void Flush(BatchingContext context)
        {
            context.Flush();
        }

        protected virtual string GetChangeComment(ChangeGrouping group)
        {
            return group.Comment + " " + CommentModifier;
        }

        protected virtual string GetChangeOwner(ChangeGrouping group)
        {
            return group.Owner;
        }

        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "1#")]
        protected virtual bool Checkin(ChangeGrouping group, out int changesetId)
        {
            changesetId = -1;
            PendingChange[] changes = Workspace.GetPendingChanges();

            Session.ThrowIfAborted();

            if (changes.Length > 0)
            {
                string comment = GetChangeComment(group);
                string owner = GetChangeOwner(group);

                PendingChange[] remainingChanges = ValidatePendingChanges(group, changes);

                if (remainingChanges.Length > 0)
                {
                    bool retryWithDefaultOwner = false;
                    try
                    {
                        changesetId = Workspace.CheckIn(
                            remainingChanges,
                            owner,
                            comment, null, null, null);

                        TraceManager.TraceInformation("Checked in change {0}", changesetId);
                    }
                    catch (IdentityNotFoundException inf)
                    {
                        TraceManager.TraceInformation(inf.Message);
                        retryWithDefaultOwner = true;
                    }
                    catch (ChangesetAuthorMustBeNonGroupException ca)
                    {
                        TraceManager.TraceInformation(ca.Message);
                        retryWithDefaultOwner = true;
                    }
                    if (retryWithDefaultOwner)
                    {

                        comment += string.Format(
                            MigrationToolkitVCResources.Culture,
                            MigrationToolkitVCResources.AuthorFallbackNote,
                            owner);

                        TraceManager.TraceInformation("Unable to checkin to TFS using the identity {0}.  Converting to default credentials.", group.Owner);

                        Session.ThrowIfAborted();

                        changesetId = Workspace.CheckIn(
                            remainingChanges,
                            comment, null, null, null);

                        TraceManager.TraceInformation("Checked in change {0}", changesetId);
                    }
                }
                else
                {
                    TraceManager.TraceInformation("All pending changes were shelved as conflicts");
                    changesetId = 0;
                }
            }
            else
            {
                TraceManager.TraceInformation("After processing there were 0 pending changes for group id {0}", group.ChangeGroupId);

                // Nothing to be checked in, 0 will be stored as changesetid in conversionhistory
                // so that we can mark this change group as migrated
                changesetId = 0;
            }

            return changesetId > -1;
        }

        protected virtual PendingChange[] ValidatePendingChanges(ChangeGrouping group, PendingChange[] changes)
        {
            int lastMigrationHWM = getLastHwm();
            string comment = string.Format(CultureInfo.InstalledUICulture, "{0}-{1}-{2}", group.SessionId, group.Direction, group.Name);
            string shelvesetName = TfsUtil.CorrectShelvesetname(comment); // TFS will throw if shelveset name is longer than 64 characters or contains illegal characters.

            List<PendingChange> conflicts = new List<PendingChange>();
            for (int i = 0; i < changes.Length; i++)
            {
                PendingChange pc = changes[i];

                if (pc.Version > lastMigrationHWM && pc.Version > m_completedTfs2SourceMigrationHWM.Value &&
                    !IsOurTfsChanges(pc.Version))
                {
                    TraceManager.TraceWarning("Detected conflict on file {0} version {1}",
                        pc.ServerItem, pc.Version);
                    conflicts.Add(pc);
                }
            }

            if (conflicts.Count > 0)
            {
                shelveConflicts(group, conflicts, shelvesetName, lastMigrationHWM, comment);
            }

            return m_workspace.GetPendingChanges();
        }

        private void shelveConflicts(ChangeGrouping group, List<PendingChange> pendingChanges, string shelvesetName, int lastTfsChange, string comment)
        {
            VersionSpec versionSpec = null;

            if (lastTfsChange == 1)
            {
                versionSpec = VersionSpec.Latest;
            }
            else
            {
                versionSpec = new ChangesetVersionSpec(lastTfsChange);
            }

            if (pendingChanges.Count > 0)
            {
                for (int i = 0; i < pendingChanges.Count; i++)
                {
                    ItemSet set = m_tfsClient.GetItems(pendingChanges[i].LocalItem,
                        versionSpec,
                        RecursionType.None,
                        DeletedState.NonDeleted,
                        ItemType.File,
                        false);

                    if (set.Items.Length == 1)
                    {
                        // Rebase file to conflicted version spec
                        m_workspace.Get(new string[] { pendingChanges[i].LocalItem }, versionSpec, RecursionType.None, GetOptions.None);
                    }
                    else
                    {
                        string msg = string.Format(CultureInfo.InvariantCulture, "{0} exists in TFS but not at version {1} (still adding to shelve)", pendingChanges[i].LocalItem, versionSpec.DisplayString);

                        // warningEmail.WriteLine(msg);
                        TraceManager.TraceWarning(msg);
                    }
                }
            }

            Conflict[] conflicts = m_workspace.QueryConflicts(null, false);
            foreach (Conflict conflict in conflicts)
            {
                TraceManager.TraceWarning(conflict.ToString());
                conflict.Resolution = Resolution.AcceptYours;
                m_workspace.ResolveConflict(conflict);
                int i = conflict.ConflictId;
            }

            Session.OnMigrationWarning(
                new VersionControlEventArgs(group, string.Format(MigrationToolkitVCResources.Culture, MigrationToolkitVCResources.ConflictShelvesetCreated, shelvesetName), SystemType.Other));

            Shelveset shelveset = new Shelveset(m_tfsClient, shelvesetName, ".");
            shelveset.Comment = comment;
            m_workspace.Shelve(shelveset, pendingChanges.ToArray(), ShelvingOptions.Move);
        }

        protected abstract bool IsOurTfsChanges(int changesetId);

        private int getLastHwm()
        {
            if (Manager.LastHighWaterMark == null)
            {
                return 1;
            }

            return int.Parse(Manager.LastHighWaterMark.ToString(), CultureInfo.CurrentCulture);
        }

        protected virtual void Download(IMigrationItem downloadItem, IMigrationAction action, string localPath)
        {
            downloadItem.Download(localPath);
        }

        protected virtual void Add(IMigrationAction action, BatchingContext context)
        {
            string localPath = null;

            try
            {
                TfsMigrationItem targetItem = (TfsMigrationItem)action.TargetTargetItem;
                localPath = TfsUtil.GetLocalPathForServerItem(targetItem.ServerPath, this.Session, useTargetPoint);

                Debug.Assert(!string.IsNullOrEmpty(localPath));

                context.PendAdd(localPath, action.SourceItem);
            }
            catch (MappingNotFoundException)
            {
                // the item is not mapped so skip it.
                action.State = ActionState.Skipped;
            }
        }

        protected virtual void Edit(IMigrationAction action, BatchingContext context)
        {
            string localPath = null;

            try
            {
                TfsMigrationItem targetItem = (TfsMigrationItem)action.TargetTargetItem;
                localPath = TfsUtil.GetLocalPathForServerItem(targetItem.ServerPath, this.Session, useTargetPoint);

                Debug.Assert(!string.IsNullOrEmpty(localPath));

                context.PendEdit(localPath, action.SourceItem);
            }
            catch (MappingNotFoundException)
            {
                // the item is not mapped so skip it.
                action.State = ActionState.Skipped;
            }
        }

        protected virtual void Delete(IMigrationAction action, BatchingContext context)
        {
            TfsMigrationItem targetItem = (TfsMigrationItem)action.TargetTargetItem;
            context.PendDelete(targetItem.ServerPath);
        }

        protected virtual void Rename(IMigrationAction action, BatchingContext context)
        {
            // download on rename is not necessary in TFS

            TfsMigrationItem targetItem = (TfsMigrationItem)action.TargetTargetItem;

            context.PendRename(
                ((TfsMigrationItem)action.TargetSourceItem).ServerPath,
                targetItem.ServerPath);
        }

        protected virtual void Branch(IMigrationAction action, BatchingContext context)
        {
            TfsMigrationItem sourceItem = (TfsMigrationItem)action.TargetSourceItem;
            TfsMigrationItem targetItem = (TfsMigrationItem)action.TargetTargetItem;
            string branchedFromVersion = TfsUtil.GetChangeIdFromConversionHistory(Session.Id, action.Version, useTargetPoint? SystemType.Other : SystemType.Tfs);

            if (string.IsNullOrEmpty(branchedFromVersion))
            {
                throw new VersionControlMigrationException(
                    string.Format(
                        MigrationToolkitVCResources.Culture,
                        MigrationToolkitVCResources.BranchParentVersionNotFound,
                        sourceItem.ServerPath, action.Version), false);
            }
            context.PendBranch(
                sourceItem.ServerPath,
                targetItem.ServerPath,
                branchedFromVersion);
        }

        protected virtual void Merge(IMigrationAction action, BatchingContext context)
        {
            string localPath = null;

            try
            {          
                TfsMigrationItem sourceItem = (TfsMigrationItem)action.TargetSourceItem;
                TfsMigrationItem targetItem = (TfsMigrationItem)action.TargetTargetItem;
                RecursionType recurse = action.Recursive ? RecursionType.Full : RecursionType.None;
                string mergeVersionFrom = TfsUtil.GetChangeIdFromConversionHistory(Session.Id, action.Version, useTargetPoint? SystemType.Other : SystemType.Tfs);
                string mergeVersionTo = TfsUtil.GetChangeIdFromConversionHistory(Session.Id, action.MergeVersionTo, useTargetPoint? SystemType.Other : SystemType.Tfs);
                if (string.IsNullOrEmpty(mergeVersionFrom))
                {
                    throw new VersionControlMigrationException(
                        string.Format(
                            MigrationToolkitVCResources.Culture,
                            MigrationToolkitVCResources.MergeVersionNotFound,
                            sourceItem.ServerPath, action.Version), false);
                }
                if (string.IsNullOrEmpty(mergeVersionTo))
                {
                    throw new VersionControlMigrationException(
                        string.Format(
                            MigrationToolkitVCResources.Culture,
                            MigrationToolkitVCResources.MergeVersionNotFound,
                            sourceItem.ServerPath, action.MergeVersionTo), false);
                }

                localPath = TfsUtil.GetLocalPathForServerItem(targetItem.ServerPath, this.Session, useTargetPoint);

                Debug.Assert(!string.IsNullOrEmpty(localPath));

                context.PendMerge(
                    ((TfsMigrationItem)action.TargetSourceItem).ServerPath,
                    targetItem.ServerPath,
                    recurse,
                    mergeVersionFrom,
                    mergeVersionTo, 
                    action.SourceItem);
            }
            catch (MappingNotFoundException)
            {
                // the item is not mapped so skip it.
                action.State = ActionState.Skipped;
            }
        }

        protected virtual void Undelete(IMigrationAction action, BatchingContext context)
        {
            TfsMigrationItem targetItem = (TfsMigrationItem)action.TargetTargetItem;
            string deletedVersion = TfsUtil.GetChangeIdFromConversionHistory(Session.Id, action.Version, useTargetPoint ? SystemType.Other : SystemType.Tfs);
            context.PendUndelete(
                ((TfsMigrationItem)action.TargetSourceItem).ServerPath, targetItem.ServerPath, deletedVersion);
        }

        protected virtual void Encoding(IMigrationAction action, BatchingContext context)
        {
            throw new NotImplementedException();
        }

        protected virtual void Label(IMigrationAction action, BatchingContext context)
        {
            TfsMigrationItem targetItem = (TfsMigrationItem)action.TargetTargetItem;
            RecursionType recurse = action.Recursive ? RecursionType.Full : RecursionType.None;

            LabelItemSpec labelItem = new LabelItemSpec(
                new ItemSpec(targetItem.ServerPath, recurse), 
                VersionSpec.Latest, false);

            Workspace.VersionControlServer.CreateLabel(
                new VersionControlLabel(Workspace.VersionControlServer,
                    action.Label,
                    action.ChangeGroup.Owner, null, action.ChangeGroup.Comment),
                    new LabelItemSpec[1] { labelItem },
                    LabelChildOption.Merge);
        }

        protected virtual void Unknown(IMigrationAction action, BatchingContext context)
        {
            Debug.Assert(false, "Unknown migration action!");

            TraceManager.TraceWarning("While processing migration action {0} an unknown action ({1}) has been encountered - skipping this action.", 
                action.ChangeGroup.ChangeGroupId,
                action.Action);
            

            return;
        }


        protected virtual Workspace Workspace
        {
            get
            {
                return m_workspace;
            }
            set
            {
                m_workspace = value;
            }
        }

        
        protected virtual Workspace CreateWorkspace()
        {
            TraceManager.EnterMethod();

            Workspace ws;

            Workstation.Current.UpdateWorkspaceInfoCache(m_tfsClient, m_tfsClient.AuthenticatedUser);

            try
            {


                ws = m_tfsClient.GetWorkspace(
                    getProperEndPoint(Session).Workspace,
                    m_tfsClient.AuthenticatedUser);

                /* the folder enum should be in the try/catch because the GetWorkspace can
                 * find a cached workspace.  if the workspace is deleted on the server the
                 * Folders call will throw WorkspaceNotFoundException which will do what
                 * we want
                 */

                // clear the existing mappings to make sure the latest ones are being used
                foreach (WorkingFolder wf in ws.Folders)
                {
                    try
                    {
                        ws.DeleteMapping(wf);
                    }
                    catch (ItemNotMappedException)
                    {
                        /* the item is no longer mapped in the workspace - ignore */
                    }
                }

                TraceManager.WriteLine(TraceManager.Engine, "Loaded existing workspace: {0}", getProperEndPoint(Session).Workspace);
            }
            catch (WorkspaceNotFoundException)
            {
                TraceManager.WriteLine(TraceManager.Engine, "Creating new workspace: {0}", getProperEndPoint(Session).Workspace);

                ws = m_tfsClient.CreateWorkspace(
                    getProperEndPoint(Session).Workspace,
                    m_tfsClient.AuthenticatedUser,
                    Session.GetValue<string>("TargetWorkspaceComment",
                        "Migration Toolkit Generated Workspace")
                    );
            }

            // determine the local root and add the mapping to the workspace
            foreach (VersionControlMapping mapping in Session.Mappings)
            {
                string localPath = TfsUtil.GetLocalPathForMapping(mapping, Session, useTargetPoint);

                if (mapping.Cloak)
                {
                    ws.Cloak(mapping.Target);
                }
                else
                {
                    ws.Map(mapping.Target, localPath);
                }
            }

            TraceManager.WriteLine(TraceManager.Engine, "Created workspace {0}", ws.Name);

            TraceManager.ExitMethod(ws);

            return ws;

        }

        VersionControlServer m_tfsClient;
        Workspace m_workspace;
        // The latest changset that has been migrated from Tfs in current session
        HighWaterMark<int> m_completedTfs2SourceMigrationHWM;
    }
}
